<?php
/**
 * API 客户端类。
 *
 * @author Nio Xiao
 * @package K12Serv
 */
class K12Serv_API_Client
{    
    protected $api_key    = null;
    protected $api_secret = null;
    protected $api_uri    = null;
    protected $debug      = false;
    
    private $errcode = 0;
    private $errmsg  = null;
    private $extra_data = array();
    
    /**
     * 构建函数。
     *
     * @param string $api_key 应用安全验证时使用的公钥
     * @param string $api_secret 应用安全验证时使用的密钥
     * @param string $api_uri 转换器 API 接口服务地址
     */
    public function __construct($api_key, $api_secret, $api_uri)
    {
        $this->api_key    = $api_key;
        $this->api_secret = $api_secret;
        $this->api_uri    = $api_uri;
    }
    
    /**
     * 获取当前调试状态。
     *
     * @return boolean
     */
    public function getDebug()
    {
        return $this->debug;
    }
    
    /**
     * 设置调试开关。
     *
     * @param boolean $flag
     * @return K12Serv_API_Client
     */
    public function setDebug($flag)
    {
        settype($flag, 'boolean');
        $this->debug = $flag;
        return $this;
    }
    
    /**
     * PING 服务器测试服务是否正常运行
     *
     * @return array|boolean 返回服务器信息数组，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function ping()
    {
        $data = array(
            'action' => 'ping',
        );
        return $this->callAPI($data);
    }

    /**
     * 获取应用信息。
     *
     * @param string $name 应用名称，在系统中唯一。
     * @return array|boolean 返回新增的应用信息数组，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function getAppInfo($name)
    {
        $data = array(
            'action'   => 'get_app_info',
            'app_name' => $name,
        );
        return $this->callAPI($data);
    }

    /**
     * 新增应用。
     *
     * @param string $name 应用名称，在系统中唯一。
     * @param string $client_ips 应用所在服务器的 IP 地址，多个IP地址使用竖线“|”分割。
     * @param string $api_key 应用公钥 API Key，也是应用的唯一标识，空则自动生成。
     * @param string $api_secret 应用私钥 API Secret，空则自动生成。
     * @param string $status 应用状态：A - 已启用，F - 已禁用，参考 K12Serv_API_Const::APP_STATUS_XXX 常量。
     * @return array|boolean 返回新增的应用信息数组，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function addApp($name, $client_ips, $api_key = null, $api_secret = null, $status = null)
    {
        $data = array(
            'action'         => 'add_app',
            'app_name'       => $name,
            'app_client_ips' => $client_ips,
            'app_api_key'    => $api_key, 
            'app_api_secret' => $api_secret, 
            'app_status'     => $status,
        );
        return $this->callAPI($data);
    }

    /**
     * 更新应用信息。
     *
     * @param string $name 应用名称，在系统中唯一，用于确定想要修改的应用。
     * @param string $client_ips 应用所在服务器的 IP 地址，多个IP地址使用竖线“|”分割，空则不更新。
     * @param string $api_key 应用公钥 API Key，也是应用的唯一标识，空则不更新。
     * @param string $api_secret 应用私钥 API Secret，空则不更新。
     * @param string $status 应用状态：A - 已启用，F - 已禁用，参考 K12Serv_API_Const::APP_STATUS_XXX 常量，空则不更新。
     * @return array|boolean 返回新增的应用信息数组，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function updateApp($name, $client_ips = null, $api_key = null, $api_secret = null, $status = null)
    {
        $data = array(
            'action'         => 'update_app',
            'app_name'       => $name,
            'app_client_ips' => $client_ips,
            'app_api_key'    => $api_key, 
            'app_api_secret' => $api_secret, 
            'app_status'     => $status,
        );
        return $this->callAPI($data);
    }

    /**
     * 获取错误代码。
     *
     * @return integer 返回错误代码，0 则标识无错误。
     */
    public function getErrorCode()
    {
        return $this->errcode;
    }
    
    /**
     * 获取错误信息。
     *
     * @return string
     */
    public function getErrorMessage()
    {
        return $this->errmsg;
    }
    
    /**
     * 当接口调用有错误时，可通过此方法获取到额外的数据（如果服务有额外数据返回的话）。
     *
     * @return array
     */
    public function getExtraData()
    {
        return $this->extra_data;
    }
    
    /**
     * 设置错误。
     *
     * @param integer $errcode 错误代码
     * @param string $errmsg 错误信息
     */
    protected function setError($errcode, $errmsg, $extra_data = array())
    {
        $this->errcode    = $errcode;
        $this->errmsg     = $errmsg;
        $this->extra_data = $extra_data;
    }
    
    /**
     * 清除错误信息。
     */
    protected function clearError()
    {
        $this->setError(0, '');
    }
    
    /**
     * 执行 API 的请求。
     *
     * @param string $action 当前请求执行的操作
     * @param array $data 请求传递的参数数组
     * @param string $method HTTP 方法
     * @return array
     */
    protected function callAPI($data, $method = 'POST')
    {
        $this->clearError();
        $data['api_key'] = $this->api_key;
        $data['format'] = 'php'; //使用 PHP 格式返回值
        if ($this->debug) $data['debug'] = 1; //打开调试开关
        $data['signature'] = self::getSignatureHash($data, $this->api_secret);
        $result = $this->httpRequest($this->api_uri, $data, $method);
        $unserialized = @unserialize($result);
        if (!$unserialized) { //服务器输出有错误
            $this->setError(999, "服务器输出格式不正确：\n".$result);
            return false;            
        } else {
            $result = $unserialized;
            if (!empty($result['errcode'])) { //返回错误
                $extra_data = isset($result['extra_data']) ? $result['extra_data'] : array();
                $this->setError($result['errcode'], $result['errmsg'], $extra_data);
                return false;
            } else { //调用成功
                return $result;
            }
        }
    }
        
    /**
     * 使用 CURL 执行 HTTP 请求。
     *
     * @param string $uri
     * @param array $data
     * @param string $method
     * @return string
     */
    private function httpRequest($uri, $data, $method = 'POST') 
    {
        if ($method == 'GET') $uri .= '?'.http_build_query($data);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $uri);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        if ($method == 'POST') {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }
        $data = curl_exec($ch);
        if ($data === false) {
            $info = curl_getinfo($ch);
            throw new Exception("CURL {$uri} 出错：" . curl_error($ch));
        } else if ($data === true){
            $info = curl_getinfo($ch);
            throw new Exception("CURL {$uri} 未返回任何数据");
        }            
        curl_close($ch);
        
        return $data;
    }

    /**
     * 提取参数数组中的所有参数值。
     *
     * @param array $params 参数数组
     * @param array $values 参数值组成的数组
     */
    private static function extractParamValues($params, &$values) 
    {
        foreach ($params as $k => $v) {
            if (is_array($v)) {
                self::extractParamValues($v, $values);
            } elseif (!is_object($v) && substr($v, 0, 1) != '@') { //对上传的文件参数不做处理
                $values[] = $v;
            }
        }
    }

    /**
     * 返回未进行哈希处理的签名字符串（api_secret + 排序之后的参数值组成的字符串）。
     * 
     * Note that the SORT_STRING option sorts case-insensitive.
     *
     * @param array $params
     * @param string $api_secret
     * @return string
     */
    private static function getSignatureString($params, $api_secret) 
    {
        $values = array();
        self::extractParamValues($params, $values);
        sort($values, SORT_STRING);
        $string = $api_secret . implode('', $values);
        return $string;
    }
    
    /**
     * 返回用于 API 调用的哈希处理之后的签名字符串。进行 API 调用时需要将此签名字符串需要作为 signature 参数。
     *
     * @param array $params
     * @param string $api_secret
     * @return string
     */
    public static function getSignatureHash($params, $api_secret) 
    {
        return md5(self::getSignatureString($params, $api_secret));
    }    
    
    /**
     * 获取文件扩展名。
     *
     * @param string $filename 文件名。
     * @return string
     */
    public static function getExt($filename)
    {
        return strpos($filename, '.') === false ? '' : strtolower(preg_replace('/[^a-z0-9]/i', '', substr($filename, strripos($filename, '.')+1)));
    }    
}
